using System;
using System.Drawing;
using System.ComponentModel;
using System.Globalization;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace GoodStuff.Web.Controls
{
	/// <summary>
	/// A custom rebuilt version of the framework's Calendar control. It was designed
	/// to work with stylesheets (instead of inline styles) and it has the ability to
	/// show week numbers, and to block dates that are out of a certain range.
	/// </summary>
	/// <remarks>
	/// This version probably contains bugs.
	/// </remarks>
	[System.ComponentModel.DefaultEvent("SelectionChanged")]
	[DefaultProperty("SelectedDate")]
	public class RangedCalendar : WebControl, IPostBackEventHandler
	{
		private System.Globalization.Calendar threadCalendar;
		private static readonly object EventSelectionChanged;
		private static readonly object EventVisibleMonthChanged;
		
		/// <summary>
		/// Contains a certain reference point in time on which postback events
		/// are based.
		/// </summary>
		private static DateTime baseDate;
		
		static RangedCalendar()
		{
			EventSelectionChanged = new object();
			EventVisibleMonthChanged = new object();
			baseDate = new DateTime(2000, 1, 1);
		}

		public RangedCalendar()
		{
			threadCalendar = System.Threading.Thread.CurrentThread.CurrentCulture.Calendar;
		}

		/// <summary>
		/// First lowest selectable date. 
		/// </summary>
		[Category("Behaviour")]
		[Description("Gets or sets the first selectable date")]
		public DateTime MinimumDate
		{
			get
			{
				object o = ViewState["MinimumDate"];
				if(o != null)
					return (DateTime)o;
				
				return DateTime.MinValue;
			}
			set
			{
				ViewState["MinimumDate"] = value;
			}
		}

		/// <summary>
		/// The highest selectable date (inclusive).
		/// </summary>
		[Category("Behaviour")]
		[Description("Gets or sets the latest selectable date")]
		public DateTime MaximumDate
		{
			get
			{
				object o = ViewState["MaximumDate"];
				if(o != null)
					return (DateTime)o;
				
				return DateTime.MaxValue;
			}
			set
			{
				ViewState["MaximumDate"] = value;
			}
		}

		/// <summary>
		/// The currently selected date.
		/// </summary>
		[Category("Behaviour")]
		[Description("Gets or sets the selected date")]
		public DateTime SelectedDate
		{
			get
			{
				object o = ViewState["SelectedDate"];
				if(o != null)
					return (DateTime)o;
				
				return DateTime.MaxValue;
			}
			set
			{
				ViewState["SelectedDate"] = value;
			}
		}

		/// <summary>
		/// The current date.
		/// </summary>
		[Category("Behaviour")]
		[Description("Gets or sets the focus date")]
		public DateTime TodaysDate
		{
			get
			{
				object o = ViewState["TodaysDate"];
				if(o != null)
					return (DateTime)o;
				
				return DateTime.Now.Date;
			}
			set
			{
				ViewState["TodaysDate"] = value;
			}
		}

		/// <summary>
		/// The current date.
		/// </summary>
		[Category("Behaviour")]
		[Description("Gets or sets the month or date currently visible")]
		public DateTime VisibleDate
		{
			get
			{
				object o = ViewState["VisibleDate"];
				if(o != null)
					return (DateTime)o;
				
				return DateTime.MinValue;
			}
			set
			{
				ViewState["VisibleDate"] = value;
			}
		}
		
		[Category("Appearance")]
		[Description("Gets or sets whether weekdays can be selected")]
		public bool AllowWeekdays
		{
			get
			{
				object o = ViewState["AllowWeekdays"];
				if(o != null) return (bool)o;

				return true;
			}
			set
			{
				ViewState["AllowWeekdays"] = value;
			}
		}

		[Category("Appearance")]
		[Description("Gets or sets whether days of the weekend can be selected")]
		public bool AllowWeekends
		{
			get
			{
				object o = ViewState["AllowWeekends"];
				if(o != null) return (bool)o;

				return true;
			}
			set
			{
				ViewState["AllowWeekends"] = value;
			}
		}

		#region Events
		public event EventHandler SelectionChanged
		{
			add
			{
				base.Events.AddHandler(EventSelectionChanged, value);
			}
			remove
			{
				base.Events.RemoveHandler(EventSelectionChanged, value);
			}
		}

		public event MonthChangedEventHandler VisibleMonthChanged
		{
			add
			{
				base.Events.AddHandler(EventVisibleMonthChanged, value);
			}
			remove
			{
				base.Events.RemoveHandler(EventVisibleMonthChanged, value);
			}
		}

		protected virtual void OnSelectionChanged()
		{
			EventHandler handler1 = (EventHandler) base.Events[EventSelectionChanged];
			if (handler1 != null)
			{
				handler1(this, new EventArgs());
			}
		}

		protected virtual void OnVisibleMonthChanged(DateTime newDate, DateTime previousDate)
		{
			MonthChangedEventHandler handler1 = (MonthChangedEventHandler) base.Events[EventVisibleMonthChanged];
			if (handler1 != null)
			{
				handler1(this, new MonthChangedEventArgs(newDate, previousDate));
			}
		}
 
		#endregion

		protected override void Render(HtmlTextWriter writer)
		{
			DateTime visibleDate = this.EffectiveVisibleDate();
			DateTime firstDayofWeek = this.FirstCalendarDay(visibleDate);

			writer.AddAttribute("border", "0");
			writer.AddAttribute("cellpadding", "0");
			writer.AddAttribute("cellspacing", "0");
			writer.AddAttribute("class", this.CssClass);
			writer.RenderBeginTag(HtmlTextWriterTag.Table);

			RenderHeader(writer, visibleDate);
			RenderDayNames(writer, firstDayofWeek);
			RenderDays(writer, visibleDate, firstDayofWeek);

			writer.RenderEndTag();
		}

		protected virtual void RenderHeader(HtmlTextWriter writer, DateTime visibleDate)
		{
			writer.AddAttribute("class", "title");
			writer.RenderBeginTag(HtmlTextWriterTag.Tr);
			
			writer.AddAttribute(HtmlTextWriterAttribute.Colspan, "8");
			writer.RenderBeginTag(HtmlTextWriterTag.Td);

			writer.AddAttribute("border", "0");
			writer.AddAttribute("cellpadding", "0");
			writer.AddAttribute("cellspacing", "0");
			writer.RenderBeginTag(HtmlTextWriterTag.Table);
			writer.Write("<tr>");
			
			//determine next focusdate.
			DateTime focusDate = threadCalendar.AddMonths(visibleDate, -1);
			TimeSpan span1 = focusDate - baseDate;

			RenderCalendarCell(writer, "NextPrevLink", "&lt;", true, "V" + span1.Days.ToString());
			
			TableItemStyle currentMonthStyle = new TableItemStyle();
			currentMonthStyle.HorizontalAlign = HorizontalAlign.Center;

			RenderCalendarCell(writer, "VisibleDate", visibleDate.ToString("MMMM yyyy"), false, null);

			focusDate = threadCalendar.AddMonths(visibleDate, 1);
			TimeSpan span2 = focusDate - baseDate;
			RenderCalendarCell(writer, "NextPrevLink", "&gt;", true, "V" + span2.Days.ToString());
			
			writer.Write("</tr>");
			writer.RenderEndTag(); //table
			writer.RenderEndTag(); //td
			writer.RenderEndTag(); //tr
		}

		protected virtual void RenderDayNames(HtmlTextWriter writer, DateTime firstDay)
		{
			DateTimeFormatInfo dateFormatInfo = DateTimeFormatInfo.CurrentInfo;

			writer.Write("<tr>");
			
			//a cell for the week numbers.
			RenderCalendarCell(writer, "DayName", "", false, null);
			
			System.DayOfWeek firstDayOfWeek = threadCalendar.GetDayOfWeek(firstDay);

			//generate 7 dayname, start with firstDayOfWeek
			for(int i = 0; i < 7; i++)
			{
				DayOfWeek weekday = (DayOfWeek)(((int)firstDayOfWeek + i)%7);
				RenderCalendarCell(writer, "DayName", dateFormatInfo.GetAbbreviatedDayName(weekday), false, null);
			}

			writer.Write("</tr>");
		}

		protected virtual void RenderDays(HtmlTextWriter writer, DateTime visibleDate, DateTime firstDayofWeek)
		{
			DateTime renderingDay = firstDayofWeek;

			//render all days in the month, starting with visibleDate
			for(int weeks = 0; weeks < 6; weeks++)
			{
				writer.Write("<tr>");
			
				//a cell for the week numbers.
				int weeknumber = threadCalendar.GetWeekOfYear(renderingDay, DateTimeFormatInfo.CurrentInfo.CalendarWeekRule, DateTimeFormatInfo.CurrentInfo.FirstDayOfWeek);
				RenderCalendarCell(writer, "WeekNumber", weeknumber.ToString(), false, null);

				//generate 7 dayname, start with firstDayOfWeek
				for(int i = 0; i < 7; i++)
				{
					TableCell cell = new TableCell();

					//TODO: delegate the context to OnRenderDay
					CalendarDay cday = new CalendarDay(renderingDay, renderingDay.DayOfWeek == DayOfWeek.Saturday || renderingDay.DayOfWeek == DayOfWeek.Sunday, renderingDay == this.TodaysDate, renderingDay == this.SelectedDate, renderingDay.Month != visibleDate.Month, renderingDay.Day.ToString());
					cday.IsSelectable = true;

					if(cday.IsOtherMonth)
						cell.CssClass = "OtherMonth";

					if(cday.IsSelected)
						cell.CssClass = "Selected";

					OnDayRender(cell, cday);

					TimeSpan span = renderingDay - baseDate;
					string eventArgument = "S" + span.Days;
 					RenderCalendarCell(writer, cell.CssClass, cday.DayNumberText, cday.IsSelectable, eventArgument);

					renderingDay = renderingDay.AddDays(1);
				}

				writer.Write("</tr>");
			}
		}

		private DateTime EffectiveVisibleDate()
		{
			DateTime time1 = this.VisibleDate;
			if (time1.Equals(DateTime.MinValue))
			{
				time1 = this.TodaysDate;
			}
			return this.threadCalendar.AddDays(time1, -(this.threadCalendar.GetDayOfMonth(time1) - 1));
		}

		private DateTime FirstCalendarDay(DateTime visibleDate)
		{
			DateTime time1 = visibleDate;
			int num1 = ((int) this.threadCalendar.GetDayOfWeek(time1)) - this.NumericFirstDayOfWeek();
			if (num1 <= 0)
			{
				num1 += 7;
			}
			return this.threadCalendar.AddDays(time1, -num1);
		}
 
		private int NumericFirstDayOfWeek()
		{
			return (int) DateTimeFormatInfo.CurrentInfo.FirstDayOfWeek;
		}
 

		protected virtual void OnDayRender(TableCell cell, CalendarDay day)
		{
			if(day.Date < MinimumDate.Date)
				day.IsSelectable = false;

			if(day.Date > MaximumDate.Date)
				day.IsSelectable = false;

			if(day.IsWeekend && !AllowWeekends)
				day.IsSelectable = false;

			if(!day.IsWeekend && !AllowWeekdays)
				day.IsSelectable = false;
		}

		private void RenderCalendarCell(HtmlTextWriter writer, string cssClass, string text, bool hasButton, string eventArgument)
		{
			writer.AddAttribute("class", cssClass);
			writer.RenderBeginTag(HtmlTextWriterTag.Td);
			if (hasButton)
			{
				writer.Write("<a href=\"");
				writer.Write(this.Page.GetPostBackClientHyperlink(this, eventArgument));
				writer.Write("\">");
				writer.Write(text);
				writer.Write("</a>");
			}
			else
			{
				writer.Write(text);
			}
			writer.RenderEndTag();
		}
		#region IPostBackEventHandler Members

		public void RaisePostBackEvent(string eventArgument)
		{
			// TODO:  Add RangedCalendar.RaisePostBackEvent implementation

			//detect visibleMonth changed
			if(eventArgument.StartsWith("V"))
			{
				int day = int.Parse(eventArgument.Substring(1));

				DateTime previousDate = VisibleDate;
				VisibleDate = baseDate.AddDays(day);

				this.OnVisibleMonthChanged(VisibleDate, previousDate);
			}
			else
			{
				if(eventArgument.StartsWith("S"))
				{
					int day = int.Parse(eventArgument.Substring(1));
					SelectedDate = baseDate.AddDays(day);

					this.OnSelectionChanged();
				}
			}
		}

		#endregion
	}
}
